package com.google.code.mochaccino.framework.hibernate;
/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.google.code.mochaccino.framework.hibernate.exception.ValidationException;
import com.google.code.mochaccino.framework.hibernate.exception.ValidationExceptionList;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.Query;
import org.hibernate.SQLQuery;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.transform.DistinctRootEntityResultTransformer;

/**
 * Abstract DAO Implementation that provides a basic sets of functionality
 * that should be common to all DAOs. This DAO implementation is tightly coupled
 * with Hibernate ORM.
 *
 * @param <T> The Type
 */
public abstract class DAOImpl<T> implements DAO<T> {
	// Entity Class
	private Class<T> entityClass = null;

	// Session Provider
	private SessionProvider provider;

	// Entity Name
	private String entityName = null;

	// The table name
	private String tableName = null;

	/** Constructor */
	protected DAOImpl( SessionProvider provider ) {
		this.provider = provider;
		entityClass = (Class<T>) (Class<?>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];

		Entity annotation = entityClass.getAnnotation( Entity.class );
		if ( annotation != null ) {
			entityName = annotation.name();
			if ( (entityName == null) || (entityName.length() == 0) ) {
				entityName = entityClass.getSimpleName();
			}
		} else {
			entityName = entityClass.getSimpleName();
		}

		Table tableAnnotation = entityClass.getAnnotation( Table.class );
		if ( tableAnnotation != null ) {
			tableName = tableAnnotation.name();
			if ( (tableName == null) || (tableName.length() == 0) ) {
				tableName = entityClass.getSimpleName();
			}
		} else {
			tableName = entityClass.getSimpleName();
		}
	}

	/** Count the number of entities in table. */
	public final long count() {
		return count( createCriteriaQuery(), false );
	}

	/** Count based on specified query */
	protected final long count( DetachedCriteria query, boolean cache ) {
		query.setProjection( Projections.rowCount() );
		List<?> result = findBy( query, cache );
		Number val = (Number) result.get( 0 );
		return val.longValue();
	}

	/** Criteria a Basic Criteria Query */
	protected final DetachedCriteria createCriteriaQuery() {
		return DetachedCriteria.forClass( getType() );
	}

	/** Criteria a Basic SQL Query */
	protected final SQLQuery createSQLQuery( String sql ) {
		return provider.getSession().createSQLQuery( sql );
	}

	/** Delete Entities */
	public final void delete( T... entities ) {
		if ( entities != null ) {
			for ( T entity : entities ) {
				deleteEntity( entity );
			}
		}
	}

	/**
	 * Delete from the datastore
	 *
	 * @see org.hibernate.Session#delete(java.lang.Object)
	 */
	protected final void deleteEntity( Object entity ) throws HibernateException {
		provider.getSession().delete( entity );
	}

	/** Delete All */
	public final int deleteAll() {
		Query q = createHQLQuery( String.format( "delete from %s", getEntityName() ) );
		return q.executeUpdate();
	}

	/** Criteria a Basic HQL Query */
	protected final Query createHQLQuery( String hql ) {
		return provider.getSession().createQuery( hql );
	}

	/**
	 * Delete from the datastore
	 *
	 * @see org.hibernate.Session#delete(java.lang.String, java.lang.Object)
	 */
	protected final void deleteEntity( String eName, Object entity ) throws HibernateException {
		provider.getSession().delete( eName, entity );
	}

	/** Evict Specified Entities */
	public final void evict( T... entities ) {
		if ( entities != null ) {
			for ( T entity : entities ) {
				if ( contains( entities ) ) {
					provider.getSession().evict( entity );
				}
			}
		}
	}

	/**
	 * Check if specified entity is associated to the session
	 *
	 * @see org.hibernate.Session#contains(java.lang.Object)
	 */
	protected final boolean contains( Object entity ) {
		return provider.getSession().contains( entity );
	}

	/** Execute Criteria */
	protected final List<?> executeQuery( DetachedCriteria query, boolean cache ) {
		Criteria q = query.getExecutableCriteria( provider.getSession() );
		prepareQuery( q, cache );
		return q.list();
	}

	/** Find All */
	public final List<T> findAll() {
		return findBy( DetachedCriteria.forClass( getType() ), false );
	}

	/** Find All */
	public final List<T> findAll( String[] joins ) {
		DetachedCriteria query = DetachedCriteria.forClass( getType() );
		if ( joins != null ) {
			query.setResultTransformer( DistinctRootEntityResultTransformer.INSTANCE );
			for ( String j : joins ) {
				query.setFetchMode( j, FetchMode.JOIN );
			}
		}
		return findBy( query, false );
	}

	/** Find By */
	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( DetachedCriteria query, boolean cache ) throws HibernateException {
		Criteria q = query.getExecutableCriteria( provider.getSession() );
		prepareQuery( q, cache );
		return q.list();
	}

	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( DetachedCriteria query ) throws HibernateException {
		return findBy( query, true );
	}

	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( Query query ) throws HibernateException {
		return findBy( query, true );
	}

	/** Find By */
	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( Query query, boolean cache ) throws HibernateException {
		prepareQuery( query, cache );
		return query.list();
	}

	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( SQLQuery query ) throws HibernateException {
		return findBy( query, true );
	}

	/** Find By */
	@SuppressWarnings( "unchecked" )
	protected final List<T> findBy( SQLQuery query, boolean cache ) throws HibernateException {
		prepareQuery( query, cache );
		return query.list();
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( DetachedCriteria query ) throws HibernateException {
		return findFirstBy( query, true );
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( DetachedCriteria query, boolean cache ) throws HibernateException {
		Criteria q = query.getExecutableCriteria( provider.getSession() );
		q.setMaxResults( 1 );
		prepareQuery( q, cache );
		List<T> results = q.list();

		if ( results == null ) {
			return null;
		}

		if ( results.size() == 0 ) {
			return null;
		}
		return results.get( 0 );
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( SQLQuery query ) throws HibernateException {
		return findFirstBy( query, true );
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( SQLQuery query, boolean cache ) throws HibernateException {
		query.setMaxResults( 1 );
		prepareQuery( query, cache );
		List<T> results = query.list();

		if ( results == null ) {
			return null;
		}

		if ( results.size() == 0 ) {
			return null;
		}

		return results.get( 0 );
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( Query query ) throws HibernateException {
		return findFirstBy( query, true );
	}

	/** Find First Results by Criteria Query */
	protected final T findFirstBy( Query query, boolean cache ) throws HibernateException {
		query.setMaxResults( 1 );
		prepareQuery( query, cache );
		List<T> results = query.list();

		if ( results == null ) {
			return null;
		}

		if ( results.size() == 0 ) {
			return null;
		}

		return results.get( 0 );
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( DetachedCriteria query ) throws HibernateException {
		return findUniqueBy( query, true );
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( DetachedCriteria query, boolean cache ) throws HibernateException {
		Criteria criteria = query.getExecutableCriteria( provider.getSession() );
		prepareQuery( criteria, cache );
		return (T) criteria.uniqueResult();
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( Query query ) throws HibernateException {
		return findUniqueBy( query, true );
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( Query query, boolean cache ) throws HibernateException {
		prepareQuery( query, cache );
		return (T) query.uniqueResult();
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( SQLQuery query ) throws HibernateException {
		return findUniqueBy( query, true );
	}

	/** Find Unique Results by Criteria Query */
	protected final T findUniqueBy( SQLQuery query, boolean cache ) throws HibernateException {
		prepareQuery( query, cache );
		return (T) query.uniqueResult();
	}

	/** Return specified entity with specified id */
	@SuppressWarnings( "unchecked" )
	public final T get( Serializable id ) {
		return (T) getEntity( getType(), id );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.Class, java.io.Serializable)
	 */
	protected final Object getEntity( Class<?> entityType, Serializable primayKey ) throws HibernateException {
		return provider.getSession().get( entityType, primayKey );
	}

	/** Return specified entity with specified id */
	@SuppressWarnings( "unchecked" )
	public final T get( Serializable id, LockOptions mode ) {
		return (T) getEntity( getType(), id, mode );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.Class, java.io.Serializable)
	 */
	protected final Object getEntity( Class<?> entityType, Serializable primayKey, LockOptions lock ) throws HibernateException {
		return provider.getSession().get( entityType, primayKey, lock );
	}

	@SuppressWarnings( "unchecked" )
	public final T get( Serializable id, LockMode mode ) {
		return (T) getEntity( getType(), id, mode );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.Class, java.io.Serializable)
	 */
	protected final Object getEntity( Class<?> entityType, Serializable primayKey, LockMode lock ) throws HibernateException {
		return provider.getSession().get( entityType, primayKey, lock );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.String, java.io.Serializable)
	 */
	protected final Object getEntity( String eName, Serializable primaryKey ) throws HibernateException {
		return provider.getSession().get( eName, primaryKey );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.String, java.io.Serializable)
	 */
	protected final Object getEntity( String eName, Serializable primaryKey, LockOptions mode ) throws HibernateException {
		return provider.getSession().get( eName, primaryKey, mode );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, or null if there is no such persistent instance.
	 *
	 * @see org.hibernate.Session#get(java.lang.String, java.io.Serializable)
	 */
	protected final Object getEntity( String eName, Serializable primaryKey, LockMode mode ) throws HibernateException {
		return provider.getSession().get( eName, primaryKey, mode );
	}

	/** Get Entity Name */
	protected final String getEntityName() {
		return entityName;
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, assuming that the instance exists.
	 *
	 * @see org.hibernate.Session#load(java.lang.Object, java.io.Serializable)
	 */
	protected final void getEntityReference( Object entity, Serializable primaryKey ) throws HibernateException {
		provider.getSession().load( entity, primaryKey );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, assuming that the instance exists.
	 *
	 * @see org.hibernate.Session#load(java.lang.Object, java.io.Serializable)
	 */
	protected final T getEntityReference( Serializable pk ) {
		return (T) provider.getSession().load( getType(), pk );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, assuming that the instance exists.
	 *
	 * @see org.hibernate.Session#load(java.lang.String, java.io.Serializable)
	 */
	protected final Object getEntityReference( String eName, Serializable primaryKey ) throws HibernateException {
		return provider.getSession().load( eName, primaryKey );
	}

	protected Criteria getExecutableCriteria( DetachedCriteria criteria ) {
		return criteria.getExecutableCriteria( provider.getSession() );
	}

	/** Return specified entity reference with specified id */
	@SuppressWarnings( "unchecked" )
	public T getReference( Serializable id ) {
		return (T) getEntityReference( getType(), id );
	}

	/**
	 * Return the persistent instance of the given entity class with the given
	 * identifier, assuming that the instance exists.
	 *
	 * @see org.hibernate.Session#load(java.lang.Class, java.io.Serializable)
	 */
	protected final Object getEntityReference( Class<?> entityType, Serializable primaryKey ) throws HibernateException {
		return provider.getSession().load( entityType, primaryKey );
	}

	/** @return the entityClass */
	public final Class<T> getType() {
		return entityClass;
	}

	/** Iterate Based on Query */
	protected Iterator<T> iterate( final Query query ) {
		return iterate( query, true );
	}

	/** Iterate Based on Query */
	protected Iterator<T> iterate( final Query query, boolean cache ) {
		prepareQuery( query, cache );
		return createIterator( query.scroll() );
	}

	/** Iterator */
	private Iterator<T> createIterator( final ScrollableResults result ) {
		return new Iterator<T>() {
			public boolean hasNext() {
				return !result.isLast();
			}

			public T next() {
				if ( result.next() ) {
					Object[] array = result.get();
					if ( array == null ) {
						return null;
					}

					if ( array.length != 1 ) {
						return null;
					}

					return (T) array[0];
				}
				return null;
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	/** Iteria All */
	public Iterator<T> iterateAll() {
		return iterate( createCriteriaQuery() );
	}

	/** Iterate Based on Query */
	protected Iterator<T> iterate( final DetachedCriteria query ) {
		return iterate( query, true );
	}

	/** Iterate Based on Query */
	protected Iterator<T> iterate( final DetachedCriteria query, boolean cache ) {
		Criteria q = query.getExecutableCriteria( provider.getSession() );
		prepareQuery( q, cache );
		return createIterator( q.scroll() );
	}

	/** Persist Entities */
	public final void persist( T... entities ) {
		if ( entities != null ) {
			for ( T entity : entities ) {
				persistEntity( entity );
			}
		}
	}

	/**
	 * <p>
	 * Make a transient instance persistent.
	 * </p>
	 * <p/>
	 * <p>
	 * The persist() method doesn't guarantee that the identifier value will be
	 * assigned to the persistent instance immediately, the assignment might
	 * happen at flush time.
	 * </p>
	 * <p/>
	 * <p>
	 * The persist() method also guarantees that it will not execute an INSERT
	 * statement if it is called outside of transaction boundaries. This is
	 * useful in long-running conversations with an extended Session/persistence
	 * context.
	 * </p>
	 *
	 * @see org.hibernate.Session#persist(java.lang.Object)
	 */
	protected final void persistEntity( Object entity ) throws HibernateException {
		Session session = provider.getSession();
		session.persist( entity );
	}

	/**
	 * <p>
	 * Make a transient instance persistent.
	 * </p>
	 * <p/>
	 * <p>
	 * The persist() method doesn't guarantee that the identifier value will be
	 * assigned to the persistent instance immediately, the assignment might
	 * happen at flush time.
	 * </p>
	 * <p/>
	 * <p>
	 * The persist() method also guarantees that it will not execute an INSERT
	 * statement if it is called outside of transaction boundaries. This is
	 * useful in long-running conversations with an extended Session/persistence
	 * context.
	 * </p>
	 *
	 * @see org.hibernate.Session#persist(java.lang.String, java.lang.Object)
	 */
	protected final void persistEntity( String eName, Object entity ) throws HibernateException {
		provider.getSession().persist( eName, entity );
	}

	/** Prepare Query */
	protected final void prepareQuery( Query query ) {
		prepareQuery( query, true );
	}

	/** Prepare Query */
	protected final void prepareQuery( SQLQuery query ) {
		prepareQuery( query, true );
	}

	/** Prepare Query */
	protected final void prepareQuery( Criteria query ) {
		prepareQuery( query, true );
	}

	/** Prepare Query */
	protected final void prepareQuery( Criteria query, boolean cache ) {
		if ( cache ) {
			query.setCacheable( true );
			query.setCacheRegion( getQueryCacheRegion() );
		}
	}

	/** Return the Query Cache */
	private String getQueryCacheRegion() {
		return getEntityName() + "_query_cache";
	}

	/** Prepare Query */
	protected final void prepareQuery( Query query, boolean cache ) {
		if ( cache ) {
			query.setCacheable( true );
			query.setCacheRegion( getQueryCacheRegion() );
		}
	}

	/** Prepare Query */
	protected final void prepareQuery( SQLQuery query, boolean cache ) {
		if ( cache ) {
			query.setCacheable( true );
			query.setCacheRegion( getQueryCacheRegion() );
		}
	}

	/**
	 * <p>
	 * Persist the given transient instance, first assigning a generated
	 * identifier.
	 * </p>
	 * <p/>
	 * <p>
	 * The save() returns an identifier, and if an INSERT has to be executed to
	 * get the identifier (e.g. "identity" generator, not "sequence"), this
	 * INSERT happens immediately, no matter if you are inside or outside of a
	 * transaction.
	 * </p>
	 * <p/>
	 * <p>
	 * This is <b>not good</b> in a long-running conversation with an extended
	 * Session/persistence context.
	 * </p>
	 *
	 * @return The Primary Key
	 * @see org.hibernate.Session#save(java.lang.Object)
	 */
	@Deprecated
	protected final Serializable saveEntity( Object entity ) throws HibernateException {
		return provider.getSession().save( entity );
	}

	/**
	 * <p>
	 * Persist the given transient instance, first assigning a generated
	 * identifier.
	 * </p>
	 * <p/>
	 * <p>
	 * The save() returns an identifier, and if an INSERT has to be executed to
	 * get the identifier (e.g. "identity" generator, not "sequence"), this
	 * INSERT happens immediately, no matter if you are inside or outside of a
	 * transaction.
	 * </p>
	 * <p/>
	 * <p>
	 * This is <b>not good</b> in a long-running conversation with an extended
	 * Session/persistence context.
	 * </p>
	 *
	 * @return The Primary Key
	 * @see org.hibernate.Session#save(java.lang.String, java.lang.Object)
	 */
	@Deprecated
	protected final Serializable saveEntity( String eName, Object entity ) throws HibernateException {
		return provider.getSession().save( eName, entity );
	}

	/**
	 * Either save(Object) or update(Object) the given instance, depending upon
	 * resolution of the unsaved-value checks (see the manual for discussion of
	 * unsaved-value checking).
	 *
	 * @see org.hibernate.Session#saveOrUpdate(java.lang.Object)
	 */
	@Deprecated
	protected final void saveOrUpdateEntity( Object entity ) throws HibernateException {
		provider.getSession().saveOrUpdate( entity );
	}

	/**
	 * Either save(Object) or update(Object) the given instance, depending upon
	 * resolution of the unsaved-value checks (see the manual for discussion of
	 * unsaved-value checking).
	 *
	 * @see org.hibernate.Session#saveOrUpdate(java.lang.String,
	 *      java.lang.Object)
	 */
	@Deprecated
	protected final void saveOrUpdateEntity( String eName, Object entity ) throws HibernateException {
		provider.getSession().saveOrUpdate( eName, entity );
	}

	/**
	 * Update the persistent instance with the identifier of the given detached
	 * instance.
	 *
	 * @see org.hibernate.Session#update(java.lang.Object)
	 */
	@Deprecated
	protected final void updateEntity( Object entity ) throws HibernateException {
		provider.getSession().update( entity );
	}

	/**
	 * Update the persistent instance with the identifier of the given detached
	 * instance.
	 *
	 * @see org.hibernate.Session#update(java.lang.String, java.lang.Object)
	 */
	@Deprecated
	protected final void updateEntity( String eName, Object entity ) throws HibernateException {
		provider.getSession().update( eName, entity );
	}

	/**
	 * Validate specified entity according to JSR303 annotations. (See Hibernate
	 * Validator for more details)
	 */
	public final boolean validate( T entity ) {
		Validator validator = provider.getValidator();
		if ( validator != null ) {
			Set<ConstraintViolation<T>> result = validator.validate( entity );
			if ( result.size() > 0 ) {
				ValidationExceptionList exList = new ValidationExceptionList( String.format( "Failed validation for entity %s", getType().getName() ) );
				for ( ConstraintViolation<T> v : result ) {
					exList.add( new ValidationException( v ) );
				}
				throw exList;
			}
			return true;
		}
		return false;
	}
}